import numpy as np
import pandas as pd

class Item_based_CF:
    def __init__(self, data):
        self.data = data
        self.ItemsForUser = {}
        self.UsersForItem = {}  # 给Item打过分的所有用户
        self.MeanForUser = {}
        self.recommendForUser = {}
        self.recommendForTest = {}  # 测试数据集
        for i in range(self.data.shape[0]):
            user = self.data.iloc[i][0]
            item = self.data.iloc[i][1]
            score = self.data.iloc[i][2]

            self.ItemsForUser.setdefault(user, {})
            self.UsersForItem.setdefault(item, {})
            self.ItemsForUser[user][item] = score
            self.UsersForItem[item][user] = score

        for key in self.ItemsForUser.keys():
            total = 0
            items = 0
            for values in self.ItemsForUser[key].values():
                total += values
                items += 1
            self.MeanForUser[key] = total / items


    def sim_cal(self, item1, item2, uid):
        common = set()
        for user in self.UsersForItem[item1]:
            if user != uid and user in self.UsersForItem[item2]:
                common.add(user)

        if len(common) == 0:
            return 0

        s1 = np.array([self.UsersForItem[item1][user] - self.MeanForUser[user] for user in common])
        s2 = np.array([self.UsersForItem[item2][user] - self.MeanForUser[user] for user in common])
        #分子
        num = np.sum(s1*s2)
        #分母
        den = np.sqrt(np.sum(s1**2)*np.sum(s2**2))
        if den == 0:
            return 0
        similarity = num / den
        return similarity


        # 预测用户uid对Item i_id的打分
    def predict(self, uid, i_id):
        sim_accumulate = 0.0
        rat_acc = 0.0
        for item in self.ItemsForUser[uid]:
            if item == i_id: #同一个item过滤掉
                continue
            sim = self.sim_cal(i_id, item, uid)
            if sim <= 0:
                continue
            rat_acc += sim * self.ItemsForUser[uid][item]
            sim_accumulate += sim

        if sim_accumulate == 0:  # no same user rated,return average rates of the data
            total = 0
            items = 0
            for value in self.ItemsForUser[uid].values():
                total += value
                items += 1
            return total/items
        return rat_acc / sim_accumulate

    def test(self, testData):
        sums = 0
        sum2 = 0
        length = testData.shape[0]

        for i in range(length):
            user = testData.iloc[i][0]
            item = testData.iloc[i][1]
            score = testData.iloc[i][2]

            self.ItemsForUser.setdefault(user, {})
            self.UsersForItem.setdefault(item, {})
            self.ItemsForUser[user][item] = score
            self.UsersForItem[item][user] = score
            
            self.recommendForTest.setdefault(user, {})
            self.recommendForUser.setdefault(user, {})

            self.recommendForTest[user][item] = score

            pre = self.predict(user, item)
            self.recommendForUser[user][item] = pre

            sums += (pre - score)**2
            sum2 += abs(pre - score)
        rmse = np.sqrt(sums / length)
        mae = sum2/length
        return rmse, mae

    def recommend(self, user, topN):
        list1 = sorted(self.recommendForUser[user].items(), key=lambda x: x[1], reverse=True)
        if topN > len(list1):
            return list1
        else:
            return list1[:topN]

    def precisionRecall(self, topN):
        hit = 0
        n_recall = 0
        n_precision = 0
        for user, items in self.recommendForUser.items():
            test = sorted(items.items(), key=lambda x: x[1], reverse=True)
            if topN > len(test):
                topN = len(test)
            testCollect = self.transformSet(test[:topN])
            recommendList = self.recommend(user, topN)
            recommendCollect = self.transformSet(recommendList)
            hit += len(testCollect.intersection(recommendCollect))
            n_recall += len(items)
            n_precision += topN
        return [hit / n_recall, hit / n_precision]

    def transformSet(self, data):
        collect = set()
        for i in data:
            collect.add(i[0])
        return collect

